<!DOCTYPE html>
<html lang="zh-CN">





<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/img/apple-touch-icon.png">
  <link rel="icon" type="image/png" href="/img/favicon.png">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="description" content="">
  <meta name="author" content="yhs0092">
  <meta name="keywords" content="ServiceComb-Java-Chassis,microservice">
  <title>【ServiceComb】微服务调用，应答返回时报ClassCastException问题的定位 ~ 遥·海·时 的博客</title>

  <link rel="stylesheet" href="https://cdn.staticfile.org/font-awesome/5.10.2/css/all.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/mdbootstrap/4.8.9/css/mdb.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/github-markdown-css/3.0.1/github-markdown.min.css"  >

<link rel="stylesheet" href="//at.alicdn.com/t/font_1067060_qzomjdt8bmp.css">



  <link rel="stylesheet" href="/lib/prettify/github-v2.min.css"  >

<link rel="stylesheet" href="/css/main.css"  >


  <link rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css"  >


<meta name="generator" content="Hexo 4.2.0"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">
    <a class="navbar-brand"
       href="/">&nbsp;<strong>遥·海·时 的博客</strong>&nbsp;</a>

    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          <li class="nav-item">
            <a class="nav-link" href="/">Home</a>
          </li>
        
          
          
          <li class="nav-item">
            <a class="nav-link" href="/archives/">Archives</a>
          </li>
        
          
          
          <li class="nav-item">
            <a class="nav-link" href="/categories/">Categories</a>
          </li>
        
          
          
          <li class="nav-item">
            <a class="nav-link" href="/tags/">Tags</a>
          </li>
        
          
          
          <li class="nav-item">
            <a class="nav-link" href="/about/">About</a>
          </li>
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" data-toggle="modal" data-target="#modalSearch">&nbsp;&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>


</nav>

    <div class="view intro-2" id="background"
         style="background: url('/img/post-banner.jpg')no-repeat center center;
           background-size: cover;
           background-attachment: fixed;">
      <div class="full-bg-img">
        <div class="mask rgba-black-light flex-center">
          <div class="container text-center white-text fadeInUp">
            <span class="h2" id="subtitle">
              
            </span>

            
              <br>
              
                <p class="mt-3">
                  <i class="fas fa-calendar-alt" aria-hidden="true"></i>&nbsp;
                  星期四, 八月 9日 2018, 6:02 晚上
                </p>
              

              <p>
                
                  
                  &nbsp;<i class="far fa-chart-bar"></i>
                  <span class="post-count">
                    2.2k 字
                  </span>&nbsp;
                

                
                  
                  &nbsp;<i class="far fa-clock"></i>
                  <span class="post-count">
                      8 分钟
                  </span>&nbsp;
                

                
                  <!-- 不蒜子统计文章PV -->
                  
                  &nbsp;<i class="far fa-eye" aria-hidden="true"></i>&nbsp;
                  <span id="busuanzi_container_page_pv">
                    <span id="busuanzi_value_page_pv"></span> 次
                  </span>&nbsp;
                
              </p>
            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid">
  <div class="row">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-md">
      <div class="py-5 z-depth-3" id="board">
        <div class="post-content mx-auto" id="post">
          <div class="markdown-body">
            <p>记录一个返回值反序列化及类型转换的问题。</p>
<a id="more"></a>
<h1 id="【ServiceComb】微服务调用，应答返回时报ClassCastException问题的定位"><a href="#【ServiceComb】微服务调用，应答返回时报ClassCastException问题的定位" class="headerlink" title="【ServiceComb】微服务调用，应答返回时报ClassCastException问题的定位"></a>【ServiceComb】微服务调用，应答返回时报ClassCastException问题的定位</h1><blockquote>
<p>本文基于CSEJavaSDK-2.3.35版本进行描述，对应的ServiceComb-Java-Chassis版本是1.1.0.B006。<br>文中的示例业务日志和代码来自<a href="https://github.com/yhs0092/CSEBlogDemo-DecodeResponseError" target="_blank" rel="noopener">问题复现demo</a>。</p>
</blockquote>
<h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>问题复现demo在<a href="https://github.com/yhs0092/CSEBlogDemo-DecodeResponseError" target="_blank" rel="noopener">这里</a>。</p>
<p>前几天被拉去看一个问题。某服务（后面称其为A服务）采用同步模式运行，RPC方式调用其他微服务。在本地调试无问题，线上运行时此服务调用另外一个服务（后面称其为B服务）的接口会报错，且通过他们自定义扩展的一个<code>HttpClientFilter</code>的日志来看，被调用的provider服务已经正常返回了应答消息，但是在后面会报<code>ClassCastException</code>，无法将<code>InvocationException</code>转型为业务代码的返回值类型。日志如下：</p>
<pre><code>// 业务逻辑被调用
[INFO] test() is called! com.github.yhs0092.blogdemo.javachassis.service.ConsumerService.test(ConsumerService.java:20)
// 用户自定义的HttpClientFilter中打印了provider返回的消息
[INFO] get response, status[200], content is [{&quot;content&quot;:&quot;returnOK&quot;}] com.github.yhs0092.blogdemo.javachassis.filter.PrintResponseFilter.afterReceiveResponse(PrintResponseFilter.java:26)
// ClassCastException被抛出
[ERROR] invoke failed, invocation=PRODUCER rest client.consumer.test org.apache.servicecomb.swagger.invocation.exception.DefaultExceptionToResponseConverter.convert(DefaultExceptionToResponseConverter.java:35)
java.lang.ClassCastException: org.apache.servicecomb.swagger.invocation.exception.InvocationException cannot be cast to com.github.yhs0092.blogdemo.javachassis.service.TestResponse
    at com.sun.proxy.$Proxy30.test(Unknown Source)
    at com.github.yhs0092.blogdemo.javachassis.service.ConsumerService.test(ConsumerService.java:21)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.doInvoke(SwaggerProducerOperation.java:160)
    at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.syncInvoke(SwaggerProducerOperation.java:148)
    at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.invoke(SwaggerProducerOperation.java:115)
    at org.apache.servicecomb.core.handler.impl.ProducerOperationHandler.handle(ProducerOperationHandler.java:40)</code></pre><p>分析问题的过程中，他们提到由于线上的B服务还是旧版本的没有升级，于是他们把A服务依赖的B服务的接口jar包替换成了低版本来启动的。</p>
<h2 id="分析过程"><a href="#分析过程" class="headerlink" title="分析过程"></a>分析过程</h2><p>初步接触这个问题给人一种很怪异的感觉。如果一个consumer调用provider时都已经拿到了应答，那么会直接把应答返回给consumer的业务逻辑代码；万一中间真的出错了，那产生的<code>InvocationException</code>也应该是被“抛”出去的，而不是像日志里面显示的那样，尝试“返回”给consumer的业务逻辑才对。</p>
<p>可供分析的信息太少了，只能回头看一下sdk代码的相关逻辑，看看能不能复现出这个问题。</p>
<p>RPC调用模式的微服务里，业务逻辑通过provider接口做调用时，实际是通过ServiceComb生成的provider接口类型的代理来做调用的。而在这个代理的背后，实际调用流程的源头在<code>org.apache.servicecomb.provider.pojo.Invoker</code>类里面。同步调用模式下，区分应答如何被返回给业务逻辑的关键代码在<code>syncInvoke</code>方法里：</p>
<pre><code class="java">protected Object syncInvoke(Invocation invocation, SwaggerConsumerOperation consumerOperation) {
  Response response = InvokerUtils.innerSyncInvoke(invocation);
  if (response.isSuccessed()) {
    // 在这里，response内的result会作为正常应答返回给业务逻辑
    return consumerOperation.getResponseMapper().mapResponse(response);
  }
  // 这里是异常逻辑，response内的result即为错误信息，会被包装为InvocationException抛给业务逻辑
  throw ExceptionFactory.convertConsumerException(response.getResult());
}</code></pre>
<p>出现了线上日志中的错误说明这个方法没有走到throw语句，而是走return语句那里返回了。</p>
<p><code>InvokerUtils.innerSyncInvoke()</code>方法里触发的主要流程是Handler-&gt;HttpClientFilter-&gt;网络线程，既然在用户自定义的<code>HTTPClientFilter</code>实现类的<code>afterReceiveResponse()</code>方法中已经打印出了B服务返回的应答消息，那么网络线程部分的嫌疑就可以排除了。问题只可能出在<code>Invoker</code>、<code>Handler</code>、<code>HTTPClientFilter</code>这三块。这个异常需要被catch住并塞到<code>response</code>里。同时，为了让异常作为response body返回，而不是被“抛”出去，<code>response.isSuccessed()</code>需要返回<code>true</code>，这就要求<code>response</code>的Http状态码必须是2xx的。通过在demo中加入自定义的<code>HttpClientFilter</code>，在<code>afterReceiveResponse()</code>方法中抛出一个状态码为200的<code>InvocationException</code>，我们复现出了这个问题，其日志特征与A服务的线上日志一致。</p>
<h2 id="根因确定"><a href="#根因确定" class="headerlink" title="根因确定"></a>根因确定</h2><p>一个response，里面装着一个异常，Http状态码却是2xx的，这个场景应该是不会发生的才对。在向A服务的开发同学确认了他们没有在自定义的<code>Handler</code>、<code>HttpClientFilter</code>内直接操作response后，我们通过日志也无法给出问题结论，只能等A服务的开发同学本地复现问题场景了。</p>
<p>好在这个问题本地是能够复现出来的，根因是在于A服务依赖的B服务接口jar包被替换后，旧版本的业务接口应答类型比新版本的多一个属性，而且这个属性的类型是找不到的，大致像下面这样：</p>
<pre><code class="java">class ResponseType {
  private InnerFieldType someField; // 这里的InnerFieldType会报ClassNotFound
}</code></pre>
<p>于是当<code>DefaultHttpClientFilter</code>的<code>extractResult()</code>方法尝试将Http body中的json串反序列化为业务代码中的应答对象时，会抛出一个异常，而这个异常被包装成<code>InvocationException</code>后，是被“return”回去的，而不是“throw”出去的，并且这个过程中没有打印任何日志。关键代码在<code>DefaultHttpClientFilter</code>的85-89行：</p>
<pre><code class="java">try {
  return produceProcessor.decodeResponse(responseEx.getBodyBuffer(), responseMeta.getJavaType());
} catch (Exception e) {
  return ExceptionFactory.createConsumerException(e); // 异常被返回
}</code></pre>
<p>“return”回去的异常被作为正常的应答对象塞进了<code>response</code>中，而<code>response</code>的状态码是Http应答的状态码——200，于是就有了线上碰到的错误。</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>ServiceComb框架在此次定位过程中暴露出来的缺少日志的问题会在后续版本中修复。但是对于开发者而言，更重要的是服务上线部署前需要做好充分验证，临时替换依赖jar包这种简单粗暴的处理方式不可取。</p>
<p>那么本地调试过程中碰到这种问题应该如何定位呢？以本文所描述的场景（RPC调用方式，同步运行模式）来看，当业务代码中触发一次微服务调用，ServiceComb的处理流程大致是：</p>
<blockquote>
<p>Invoker -&gt; InvokerUtils -&gt; Handler -&gt; HTTPClientFilter -&gt; 网络线程</p>
</blockquote>
<p><code>Invoker</code>是RPC调用模式下的动态代理，业务代码通过provider接口做调用时，参数首先被传到<code>invoke()</code>方法中。由于consumer工作于同步模式，<code>Invoker</code>会通过<code>syncInvoke()</code>方法调用<code>InvokerUtils</code>的<code>innerSyncInvoke()</code>方法。在这里，<code>Invocation</code>的<code>next()</code>方法被调用，<strong><em>从而触发Handler链执行</em></strong>。在Handler链的末尾是<code>TransportClientHandler</code>，它会调用对应的transport方式发送请求。在Rest over Vertx传输方式下，我们需要关注的是<code>RestClientInvocation</code>的<code>invoke()</code>方法，这里会<strong><em>遍历执行HttpClientFilter的beforeSendRequest()方法</em></strong>，然后将请求调度到网络线程中发送。业务线程此时处于等待返回的状态（<code>SyncResponseExecutor.waitResponse()</code>方法中使用<code>CountDownLatch</code>进行等待）。</p>
<p>当请求应答返回后，<code>RestClientInvocation.processResponseBody()</code>方法会将Http response body返回给业务线程处理（通过触发<code>SyncResponseExecutor</code>的<code>CountDownLatch</code>）。应答首先会在<code>RestClientInvocation</code>中<strong><em>遍历HttpClientFilter的afterReceiveResponse()方法</em></strong>进行处理，然后<strong><em>经过Handler链</em></strong>的回调处理，最终返回给<code>InvokerUtils</code>的<code>syncInvoke()</code>方法。其中，<strong><em>Http response body是在DefaultHttpClientFilter的extractResult()方法中反序列化为业务接口返回对象的</em></strong>。这个方法会根据<code>response</code>的HTTP状态码判断如何对待结果，如果是2xx的状态码，则<code>response</code>中的<code>result</code>会作为正常的应答返回给业务逻辑，否则会将<code>result</code>包装到<code>InvocationException</code>中抛给业务逻辑。</p>
<pre><code class="java">  // RestClientInvocation中处理应答的关键方法
  protected void processResponseBody(Buffer responseBuf) {
    invocation.getResponseExecutor().execute(() -&gt; {
      // 同步模式下，应答返回流程从这里开始就是在业务线程里执行的
      try {
        HttpServletResponseEx responseEx =
            new VertxClientResponseToHttpServletResponse(clientResponse, responseBuf);
        for (HttpClientFilter filter : httpClientFilters) {
          // HttpClientFilter处理返回消息体，普通的filter会返回null
          Response response = filter.afterReceiveResponse(invocation, responseEx);
          if (response != null) { // DefaultHttpClientFilter会把消息体反序列化为应答对象，装入response返回
            asyncResp.complete(response); // 通过回调触发handler链
            return;
          }
        }
      } catch (Throwable e) {
        asyncResp.fail(invocation.getInvocationType(), e); // 包装异常，通过回调触发handler链
      }
    });
  }</code></pre>
<p>本地分析这类问题的时候，首先需要知道请求发送的流程，了解RPC动态代理的入口、<code>Handler</code>链的起止点、<code>HttpClientFilter</code>的调用点。这些是流程中的关键节点，根据这些信息可以大致确定问题出现的范围。至于更进一步的定位，就需要大家根据具体的问题进行分析了。</p>
<blockquote>
<p>看不懂上面说的是什么？正常 :P</p>
<p>只看一篇博客是很难弄懂这段流程的。关键的代码节点已经给出来了，自己写个demo调试一下，你的了解会更深刻</p>
<p>(￣▽￣)ﾉ</p>
</blockquote>

            <hr>
          </div>
          <br>
          <div>
            <p>
            
              <span>
                <i class="iconfont icon-inbox"></i>
                
                  <a class="hover-with-bg" href="/categories/%E8%BD%AF%E4%BB%B6%E6%8A%80%E6%9C%AF">软件技术</a>
                  &nbsp;
                
                  <a class="hover-with-bg" href="/categories/%E8%B8%A9%E5%9D%91">踩坑</a>
                  &nbsp;
                
              </span>&nbsp;&nbsp;
            
            
              <span>
                <i class="iconfont icon-tag"></i>
                
                  <a class="hover-with-bg" href="/tags/microservice">microservice</a>
                
                  <a class="hover-with-bg" href="/tags/ServiceComb-Java-Chassis">ServiceComb-Java-Chassis</a>
                
              </span>
            
            </p>
            
              <p class="note note-warning">本博客所有文章除特别声明外，均采用 <a href="https://zh.wikipedia.org/wiki/Wikipedia:CC_BY-SA_3.0%E5%8D%8F%E8%AE%AE%E6%96%87%E6%9C%AC" target="_blank" rel="nofollow noopener noopener">CC BY-SA 3.0协议</a> 。转载请注明出处！</p>
            
          </div>
        </div>
      </div>
    </div>
    <div class="d-none d-lg-block col-lg-2 toc-container">
      
  <div id="toc">
    <p class="h4"><i class="far fa-list-alt"></i>&nbsp;目录</p>
    <div id="tocbot"></div>
  </div>

    </div>
  </div>
</div>

<!-- custom -->


<!-- Comments -->
<div class="col-lg-7 mx-auto nopadding-md">
  <div class="container comments mx-auto" id="comments">
    
  </div>
</div>

    
  </main>

  
    <a class="z-depth-1" id="scroll-top-button" href="#" role="button">
      <i class="fa fa-chevron-up scroll-top-arrow" aria-hidden="true"></i>
    </a>
  

  
    <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
  

  <footer class="mt-5">
  <div class="text-center py-3">
    <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><b>Hexo</b></a>
    <i class="iconfont icon-love"></i>
    <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener"> <b>Fluid</b></a>
    <br>

    
  
    <!-- 不蒜子统计PV -->
    
    &nbsp;<span id="busuanzi_container_site_pv">总访问量 
          <span id="busuanzi_value_site_pv"></span> 次</span>&nbsp;
  
  
    <!-- 不蒜子统计UV -->
    
    &nbsp;<span id="busuanzi_container_site_uv">总访客数 
            <span id="busuanzi_value_site_uv"></span> 人</span>&nbsp;
  
  <br>



    

  </div>
</footer>

<!-- SCRIPTS -->
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js" ></script>
<script src="https://cdn.staticfile.org/popper.js/1.15.0/umd/popper.min.js" ></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/js/bootstrap.min.js" ></script>
<script src="https://cdn.staticfile.org/mdbootstrap/4.8.9/js/mdb.min.js" ></script>
<script src="/js/main.js" ></script>


  <script src="/js/lazyload.js" ></script>



  
    <script src="https://cdn.staticfile.org/tocbot/4.8.0/tocbot.min.js" ></script>
  
  <script src="/js/post.js" ></script>



  <script src="https://cdn.staticfile.org/smooth-scroll/16.1.0/smooth-scroll.min.js" ></script>



  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" ></script>


<!-- Plugins -->


  

  

  

  

  




  <script src="https://cdn.staticfile.org/prettify/r298/prettify.min.js" ></script>
  <script>
    $(document).ready(function () {
      $('pre').addClass('prettyprint  linenums');
      prettyPrint();
    })
  </script>



  <script src="https://cdn.staticfile.org/typed.js/2.0.10/typed.min.js" ></script>
  <script>
    var typed = new Typed('#subtitle', {
      strings: [
        '  ',
        "【ServiceComb】微服务调用，应答返回时报ClassCastException问题的定位&nbsp;",
      ],
      cursorChar: "_",
      typeSpeed: 70,
      loop: false,
    });
    typed.stop();
    $(document).ready(function () {
      $(".typed-cursor").addClass("h2");
      typed.start();
    });
  </script>



  <script src="https://cdn.staticfile.org/anchor-js/4.2.0/anchor.min.js" ></script>
  <script>
    anchors.options = {
      placement: "left",
      visible: "false",
      
    };
    var el = "h1,h2,h3,h4,h5,h6".split(",");
    var res = [];
    for (item of el) {
      res.push(".markdown-body > " + item)
    }
    anchors.add(res.join(", "))
  </script>



  <script src="/js/local-search.js" ></script>
  <script>
    var path = "/local-search.xml";
    var inputArea = document.querySelector("#local-search-input");
    inputArea.onclick = function () {
      getSearchFile(path);
      this.onclick = null
    }
  </script>



  <script src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js" ></script>
  <script>
    $("#post img:not(.no-zoom img, img[no-zoom])").each(
      function () {
        var element = document.createElement("a");
        $(element).attr("data-fancybox", "images");
        $(element).attr("href", $(this).attr("src"));
        $(this).wrap(element);
      }
    );
  </script>







</body>
</html>
